Skip to content

feat(file): add Manage Sharing operation to the File block#5177

Merged
TheodoreSpeaks merged 6 commits into
stagingfrom
feat/file-permission-block
Jun 24, 2026
Merged

feat(file): add Manage Sharing operation to the File block#5177
TheodoreSpeaks merged 6 commits into
stagingfrom
feat/file-permission-block

Conversation

@TheodoreSpeaks

@TheodoreSpeaks TheodoreSpeaks commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add a Manage Sharing operation (file_manage_sharing) to the File block (file_v5) so a workflow/agent can enable, disable, or reconfigure a file's public share link as a step.
  • Idempotent: reuses the existing upsertFileShare domain logic, so the public link (token) stays stable across enable/disable/reconfigure.
  • Full access-mode parity with the UI via a single Visibility dropdown: private (disable), public, password, email, sso — with conditional password / allowed-emails fields.
  • New manage_sharing case on /api/tools/file/manage, a file_manage_sharing tool, and a contract schema (fileId or fileInput, mirroring read/get-content). No existing operations changed.

Behavior & security

  • Requires workspace write/admin (publishing is more sensitive than the other mutating ops), checked before the file lookup so a read-only caller can't probe file existence via 404-vs-403.
  • Enabling is gated by the org's EE public-sharing policy; the gate resolves authType from the existing share (authType ?? existing ?? 'public') so a restricted share can't be re-enabled under a public-only policy without an explicit type. Disabling is never gated.
  • isActive is explicit/required (no silent enable on a bare call). isActive/authType/allowedEmails are agent-controllable (user-or-llm); password stays user-only.
  • Resolves the target file from a canonical id, an upstream file reference, or the basic file picker's object (maps its storage key → canonical id via getFileMetadataByKey). Rejects multiple files. Setting a file private returns an empty url (no dead link).

Type of Change

  • New feature

Testing

  • Unit tests for the operation's param mapping: public / private / password / email visibility, canonical-id and file-object resolution, basic-picker (key-only) → fileInput, multiple-file rejection, missing-file error.
  • tsc --noEmit, bun run lint, bun run check:api-validation:strict, and the block-registry invariant check all pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Adds a new file_set_sharing operation to the File block (file_v5) that
idempotently enables/disables a file's public share link and sets its
access mode (public, password, email, SSO). The set_sharing route case
reuses upsertFileShare, requires write/admin, gates enabling through the
EE public-sharing policy, and records a share audit. Returns an empty url
when set to private so a disabled link isn't handed back as a dead link.
@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 24, 2026 12:23am

Request Review

@cursor

cursor Bot commented Jun 23, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Workflows and tools can now publish or reconfigure public file access (including passwords and allowlists), which is security- and policy-sensitive; mitigations include write/admin checks, EE policy on enable, and audit logging.

Overview
Adds a Manage Sharing operation to the file_v5 File block so workflows can turn public share links on or off and set access modes (public, password, email, SSO) for a single workspace file.

The block gets a new operation, file picker / file ID inputs, a Visibility dropdown with conditional password and allowlist fields, param mapping into file_manage_sharing, and share-related outputs (url, isActive, authType, etc.). A new file_manage_sharing tool posts manage_sharing to /api/tools/file/manage, with Zod contract validation and registry wiring.

The API route adds a manage_sharing branch that requires write/admin before file lookup (reducing existence side channels), resolves file ID from picker objects via storage key when needed, gates enabling shares through EE validatePublicFileSharing, calls upsertFileShare, audits share enable/disable, and returns an empty url when the link is disabled. ShareValidationError is mapped to 400.

Unit tests cover block param mapping and the v5 operation list.

Reviewed by Cursor Bugbot for commit 8c2750d. Bugbot is set up for automated code reviews on this repo. Configure here.

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

Comment thread apps/sim/lib/api/contracts/tools/file.ts Outdated
@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a Manage Sharing operation to the file_v5 block (file_manage_sharing tool + manage_sharing route case) that idempotently enables or disables a workspace file's public share link and configures its access mode (public, password, email, SSO). The implementation reuses the existing upsertFileShare domain logic, gates enabling on the org's EE policy via validatePublicFileSharing, and records FILE_SHARED/FILE_SHARE_DISABLED audit events. Previous review concerns (permission-before-file-lookup ordering, auth-type fallback for the policy gate, and the isActive interface alignment) are correctly addressed in this version.

  • Route handler (manage/route.ts): write/admin permission check precedes file lookup; getShareForResource is used to resolve the existing share's authType so the policy gate cannot be bypassed by re-enabling a pre-existing restricted share without an explicit authType.
  • Block & tool wiring (file.ts, manage-sharing.ts): isActive, authType, and allowedEmails are correctly user-or-llm visibility; password is user-only. Sub-block conditional fields appear only for the relevant visibility modes.
  • Contract (contracts/tools/file.ts): isActive is a required z.boolean(), and the .refine() ensures at least one of fileId/fileInput is present; test coverage covers all major paths.

Confidence Score: 5/5

The feature is safe to merge; the security-sensitive paths (permission ordering, auth-type policy bypass) flagged in earlier review rounds are correctly addressed in this version.

All three structural concerns raised in previous review threads — permission check before file lookup, auth-type fallback consistency for the policy gate, and the isActive interface alignment — are resolved. The remaining observations are a minor redundant DB read and an audit-action granularity gap, neither of which affects correctness or security.

apps/sim/app/api/tools/file/manage/route.ts — the manage_sharing case makes two sequential reads against the publicShare table when enabling a share; worth a follow-up to deduplicate.

Important Files Changed

Filename Overview
apps/sim/app/api/tools/file/manage/route.ts Adds the manage_sharing case: permission-before-lookup ordering is correct, authType fallback for policy gate mirrors upsertFileShare's own logic, and ShareValidationError is caught globally. The getShareForResource call (for policy) and upsertFileShare's internal share fetch are redundant DB reads for the same row.
apps/sim/tools/file/manage-sharing.ts Tool definition for file_manage_sharing; isActive/authType/allowedEmails correctly marked user-or-llm, only password is user-only. Interface correctly declares isActive: boolean (required).
apps/sim/lib/api/contracts/tools/file.ts fileManageSharingBodySchema correctly validates isActive as required boolean, enforces either fileId or fileInput via refine, and defers auth-type enum validation to shareAuthTypeSchema.
apps/sim/blocks/blocks/file.ts Adds Manage Sharing operation with correct sub-block wiring, conditional fields (password/email), and params mapping that resolves file IDs from picker objects or canonical strings.
apps/sim/blocks/blocks/file.test.ts Good test coverage for the new operation: public/private/password/email paths, file object resolution (with and without id), multi-file rejection, and no-input guard.
apps/sim/blocks/blocks.test.ts Adds file_manage_sharing to the tool access list assertion; straightforward registry update.
apps/sim/tools/file/index.ts Re-exports fileManageSharingTool; no issues.
apps/sim/tools/registry.ts Registers file_manage_sharing in the tools map; straightforward addition.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client as Block / LLM Agent
    participant Route as POST /api/tools/file/manage
    participant DB1 as DB (permissions)
    participant DB2 as DB (files)
    participant DB3 as DB (shares — policy)
    participant Policy as validatePublicFileSharing
    participant DB4 as DB (shares — upsert)
    participant Audit as recordAudit (fire-and-forget)

    Client->>Route: "{ operation: manage_sharing, fileId, isActive, authType, ... }"
    Route->>Route: assertActiveWorkspaceAccess
    Route->>DB1: getUserEntityPermissions(userId, workspace)
    DB1-->>Route: "permission (admin | write | read)"
    alt "permission == read"
        Route-->>Client: 403 Insufficient permissions
    end
    Route->>Route: resolveFileId (fileId / fileInput.id / fileInput.fileId / fileInput.key)
    Route->>DB2: getWorkspaceFile(workspaceId, resolvedFileId)
    DB2-->>Route: "file | null"
    alt not found
        Route-->>Client: 404 File not found
    end
    alt "isActive == true"
        Route->>DB3: getShareForResource('file', resolvedFileId)
        DB3-->>Route: "existingShare | null"
        Route->>Route: "resolvedAuthType = authType ?? existingShare?.authType ?? 'public'"
        Route->>Policy: validatePublicFileSharing(userId, workspaceId, resolvedAuthType)
        alt policy denied
            Route-->>Client: 403 sharing not allowed
        end
    end
    Route->>DB4: "upsertFileShare({ workspaceId, fileId, isActive, authType, password, allowedEmails })"
    Note over DB4: internally re-fetches existing share (second DB read for same row)
    DB4-->>Route: ShareRecord
    Route-)Audit: "recordAudit FILE_SHARED | FILE_SHARE_DISABLED"
    Route-->>Client: "{ success: true, data: { share: { url, isActive, authType, ... } } }"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client as Block / LLM Agent
    participant Route as POST /api/tools/file/manage
    participant DB1 as DB (permissions)
    participant DB2 as DB (files)
    participant DB3 as DB (shares — policy)
    participant Policy as validatePublicFileSharing
    participant DB4 as DB (shares — upsert)
    participant Audit as recordAudit (fire-and-forget)

    Client->>Route: "{ operation: manage_sharing, fileId, isActive, authType, ... }"
    Route->>Route: assertActiveWorkspaceAccess
    Route->>DB1: getUserEntityPermissions(userId, workspace)
    DB1-->>Route: "permission (admin | write | read)"
    alt "permission == read"
        Route-->>Client: 403 Insufficient permissions
    end
    Route->>Route: resolveFileId (fileId / fileInput.id / fileInput.fileId / fileInput.key)
    Route->>DB2: getWorkspaceFile(workspaceId, resolvedFileId)
    DB2-->>Route: "file | null"
    alt not found
        Route-->>Client: 404 File not found
    end
    alt "isActive == true"
        Route->>DB3: getShareForResource('file', resolvedFileId)
        DB3-->>Route: "existingShare | null"
        Route->>Route: "resolvedAuthType = authType ?? existingShare?.authType ?? 'public'"
        Route->>Policy: validatePublicFileSharing(userId, workspaceId, resolvedAuthType)
        alt policy denied
            Route-->>Client: 403 sharing not allowed
        end
    end
    Route->>DB4: "upsertFileShare({ workspaceId, fileId, isActive, authType, password, allowedEmails })"
    Note over DB4: internally re-fetches existing share (second DB read for same row)
    DB4-->>Route: ShareRecord
    Route-)Audit: "recordAudit FILE_SHARED | FILE_SHARE_DISABLED"
    Route-->>Client: "{ success: true, data: { share: { url, isActive, authType, ... } } }"
Loading

Reviews (7): Last reviewed commit: "fix(file): resolve basic-picker files fo..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a Set File Sharing operation to the file_v5 block, wiring a new file_set_sharing tool through a set_sharing API case that idempotently enables/disables a file's public share link with configurable access modes (public, password, email, SSO).

  • The block, tool, and contract schema are cleanly structured: conditional password/email subblocks, correct user-only visibility on sensitive params, a Zod schema with sensible field constraints, and a url: '' sentinel on disabled shares.
  • The route adds write/admin permission enforcement and EE access-control policy gating, but contains a logic gap: validatePublicFileSharing is called with authType ?? 'public' while upsertFileShare resolves the effective auth type as authType ?? existingShare.authType ?? 'public' — diverging when authType is omitted and an existing share has a restricted auth mode.

Confidence Score: 3/5

The block, tool, and contract changes are safe to merge; the route's set_sharing handler needs a fix before shipping to production.

The set_sharing route validates the effective sharing auth type against the org's EE policy using 'public' as a hard-coded fallback when authType is absent. upsertFileShare independently computes the real effective auth type from the existing share row, so the two values can diverge: a user on a workspace that restricts sharing to 'public' only can re-enable a previously-disabled 'sso' share by omitting authType, bypassing the allowedFileShareAuthTypes gate. The rest of the PR — tool definition, block wiring, contract schema, and tests — is well-implemented and low-risk.

apps/sim/app/api/tools/file/manage/route.ts — the set_sharing switch case needs the effective auth type resolved (from the existing share record) before calling validatePublicFileSharing.

Security Review

  • allowedFileShareAuthTypes policy bypass (apps/sim/app/api/tools/file/manage/route.ts, set_sharing case): validatePublicFileSharing is called with authType ?? 'public' as the hard-coded fallback, but upsertFileShare resolves the effective auth type as authType ?? existingShare.authType ?? 'public'. On an enterprise workspace that permits only public sharing, a user can re-enable a previously-disabled sso or email share by omitting authType from the request: the policy check sees 'public' (allowed) but the upsert persists the old 'sso' or 'email' type.
  • File-existence side-channel for read-only users (apps/sim/app/api/tools/file/manage/route.ts, set_sharing case): the file lookup occurs before the write/admin permission check, leaking whether a file ID exists via 404 vs 403 differential to callers who only have read-level workspace access.

Important Files Changed

Filename Overview
apps/sim/app/api/tools/file/manage/route.ts Adds set_sharing case with write/admin guard, EE policy validation, and audit logging; two issues: policy check uses wrong fallback authType for existing shares (allowing auth-type bypass), and file existence is probed before the permission check.
apps/sim/tools/file/set-sharing.ts New tool definition for file_set_sharing; well-structured with correct visibility settings and proper response transformation.
apps/sim/blocks/blocks/file.ts Adds file_set_sharing operation to FileV5Block with correct conditional subblocks, params mapping, and output declarations; password/email/SSO conditional fields are wired correctly.
apps/sim/lib/api/contracts/tools/file.ts New fileManageSetSharingBodySchema added to the union; field constraints (min lengths, max 200 emails, max 1024 password) look sensible.
apps/sim/blocks/blocks/file.test.ts Good coverage of public/private/password/email visibility mappings and file-object resolution; missing a test case for the sso visibility path.
apps/sim/tools/registry.ts Registers file_set_sharing tool in the correct alphabetical position.
apps/sim/blocks/blocks.test.ts Adds file_set_sharing to the known-tools assertion; straightforward and correct.
apps/sim/tools/file/index.ts Exports the new fileSetSharingTool following the existing barrel pattern.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Block as FileV5Block (file.ts)
    participant Tool as fileSetSharingTool (set-sharing.ts)
    participant Route as /api/tools/file/manage
    participant Auth as checkInternalAuth
    participant Perm as getUserEntityPermissions
    participant Policy as validatePublicFileSharing
    participant DB as upsertFileShare (share-manager)
    participant Audit as recordAudit

    Block->>Tool: "buildParams() → { fileId, isActive, authType, password, allowedEmails }"
    Tool->>Route: "POST { operation: 'set_sharing', ... }"
    Route->>Auth: checkInternalAuth(request)
    Auth-->>Route: "{ userId }"
    Route->>Route: assertActiveWorkspaceAccess(workspaceId, userId)
    Route->>DB: getWorkspaceFile(workspaceId, fileId)
    DB-->>Route: "file | null (404 if missing)"
    Route->>Perm: getUserEntityPermissions(userId, 'workspace', workspaceId)
    Perm-->>Route: "'admin' | 'write' | 'read' (403 if read)"
    alt "isActive = true"
        Route->>Policy: validatePublicFileSharing(userId, workspaceId, authType ?? 'public')
        Note over Route,Policy: uses 'public' fallback, not existing share's authType
        Policy-->>Route: throws PublicFileSharingNotAllowedError on policy violation
    end
    Route->>DB: "upsertFileShare({ workspaceId, fileId, isActive, authType, ... })"
    Note over DB: finalAuthType = authType ?? existing.authType ?? 'public'
    DB-->>Route: ShareRecord
    Route->>Audit: "recordAudit(FILE_SHARED | FILE_SHARE_DISABLED) fire-and-forget"
    Route-->>Tool: "{ success: true, data: { share } }"
    Tool-->>Block: "{ url, isActive, authType, hasPassword, allowedEmails }"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Block as FileV5Block (file.ts)
    participant Tool as fileSetSharingTool (set-sharing.ts)
    participant Route as /api/tools/file/manage
    participant Auth as checkInternalAuth
    participant Perm as getUserEntityPermissions
    participant Policy as validatePublicFileSharing
    participant DB as upsertFileShare (share-manager)
    participant Audit as recordAudit

    Block->>Tool: "buildParams() → { fileId, isActive, authType, password, allowedEmails }"
    Tool->>Route: "POST { operation: 'set_sharing', ... }"
    Route->>Auth: checkInternalAuth(request)
    Auth-->>Route: "{ userId }"
    Route->>Route: assertActiveWorkspaceAccess(workspaceId, userId)
    Route->>DB: getWorkspaceFile(workspaceId, fileId)
    DB-->>Route: "file | null (404 if missing)"
    Route->>Perm: getUserEntityPermissions(userId, 'workspace', workspaceId)
    Perm-->>Route: "'admin' | 'write' | 'read' (403 if read)"
    alt "isActive = true"
        Route->>Policy: validatePublicFileSharing(userId, workspaceId, authType ?? 'public')
        Note over Route,Policy: uses 'public' fallback, not existing share's authType
        Policy-->>Route: throws PublicFileSharingNotAllowedError on policy violation
    end
    Route->>DB: "upsertFileShare({ workspaceId, fileId, isActive, authType, ... })"
    Note over DB: finalAuthType = authType ?? existing.authType ?? 'public'
    DB-->>Route: ShareRecord
    Route->>Audit: "recordAudit(FILE_SHARED | FILE_SHARE_DISABLED) fire-and-forget"
    Route-->>Tool: "{ success: true, data: { share } }"
    Tool-->>Block: "{ url, isActive, authType, hasPassword, allowedEmails }"
Loading

Reviews (1): Last reviewed commit: "feat(file): add Set File Sharing operati..." | Re-trigger Greptile

Comment thread apps/sim/tools/file/manage-sharing.ts
Comment thread apps/sim/app/api/tools/file/manage/route.ts
Comment thread apps/sim/app/api/tools/file/manage/route.ts Outdated
… params, policy gate + perm-check ordering

Addresses review findings:
- Make isActive explicit/required so a bare call no longer silently enables a public link
- Expose isActive/authType/allowedEmails as user-or-llm so agents can disable/configure shares (password stays user-only)
- Resolve authType from the existing share before the EE policy gate to close a re-enable bypass
- Run the write/admin permission check before the file lookup to remove a file-existence side channel
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

Addressed the review findings in 75f229f (Bugbot re-ran clean; the Greptile notes above are the original review re-anchored to the new commit):

  • isActive silently enabling sharingisActive is now explicit/required in the contract (no .default(true)), so a bare call can't accidentally publish.
  • Agents can't control share stateisActive, authType, and allowedEmails are now user-or-llm (only password stays user-only).
  • Policy gate fallback authType — the gate now resolves authType ?? existingShare.authType ?? 'public' (via getShareForResource) before validatePublicFileSharing, matching what upsertFileShare persists, closing the re-enable bypass.
  • File-existence side channel — the write/admin permission check now runs before getWorkspaceFile.

Renames the file_set_sharing operation to file_manage_sharing (route literal
manage_sharing, tool Manage Sharing) across the contract, route, tool, block,
registry, and tests.
@TheodoreSpeaks TheodoreSpeaks changed the title feat(file): add Set File Sharing operation to the File block feat(file): add Manage Sharing operation to the File block Jun 23, 2026
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

Renamed the operation to Manage Sharing (file_manage_sharing / route manage_sharing) in e175b6d — no behavior change.

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

Prior commit's lint-staged dropped the barrel re-export update, leaving
index.ts importing the deleted set-sharing module and breaking the block
registry check. Point the barrel at manage-sharing.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

Comment thread apps/sim/tools/file/manage-sharing.ts Outdated
Comment thread apps/sim/blocks/blocks/file.ts Outdated
…s in manage sharing

- FileManageSharingParams.isActive is now required, matching the tool param
  and contract (no compile-time gap that 400s at runtime)
- manage_sharing rejects multiple canonical file IDs instead of silently
  sharing only the first, matching decompress
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 80bb8ed. Configure here.

Comment thread apps/sim/blocks/blocks/file.ts Outdated
The basic file-upload picker stores a workspace file as { name, path, key,
size, type } with no canonical id, so manage_sharing failed those picks with
'Could not determine the file to share'. The block now passes the picked
object as fileInput when it lacks an id, and the route resolves the canonical
id from the storage key via getFileMetadataByKey. Contract accepts fileId OR
fileInput (mirroring read/get content).
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks TheodoreSpeaks merged commit 4554df9 into staging Jun 24, 2026
16 checks passed
@TheodoreSpeaks TheodoreSpeaks deleted the feat/file-permission-block branch June 24, 2026 01:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant